{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "# 用Flask发送电子邮件\r\n",
    "\r\n",
    "\r\n",
    "Web 应用程序一直在发送电子邮件，在本课中，我们将把电子邮件发送功能集成到我们的 Flask 应用程序中。\r\n",
    "\r\n",
    "Python 标准库有一个名为 `smtplib` 的模块，可用于发送电子邮件。尽管直接使用 `smtplib` 模块并不复杂，但仍然需要您做一些工作。为了简化这个过程，人们创建了一个名为 `Flask-Mail` 的扩展。`Flask-Mail` 是围绕 Python `smtplib` 模块构建的，它公开了一个用于发送电子邮件的简单界面。它还支持大量的电子邮件和附件。使用以下命令安装 Flask-Mail:"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "source": [
    "!pip install flask-mail"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Looking in indexes: https://pypi.douban.com/simple\n",
      "Requirement already satisfied: flask-mail in e:\\python39\\lib\\site-packages (0.9.1)\n",
      "Requirement already satisfied: Flask in e:\\python39\\lib\\site-packages (from flask-mail) (1.1.3)\n",
      "Requirement already satisfied: blinker in e:\\python39\\lib\\site-packages (from flask-mail) (1.4)\n",
      "Requirement already satisfied: Jinja2<3.0,>=2.10.1 in e:\\python39\\lib\\site-packages (from Flask->flask-mail) (2.11.3)\n",
      "Requirement already satisfied: itsdangerous<2.0,>=0.24 in e:\\python39\\lib\\site-packages (from Flask->flask-mail) (1.1.0)\n",
      "Requirement already satisfied: click<8.0,>=5.1 in e:\\python39\\lib\\site-packages (from Flask->flask-mail) (7.1.2)\n",
      "Requirement already satisfied: Werkzeug<2.0,>=0.15 in e:\\python39\\lib\\site-packages (from Flask->flask-mail) (1.0.1)\n",
      "Requirement already satisfied: MarkupSafe>=0.23 in e:\\python39\\lib\\site-packages (from Jinja2<3.0,>=2.10.1->Flask->flask-mail) (2.0.1)\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "从 `flask_mail` 包初始化扩展导入 `Mail` 类并创建一个 `Mail` 类实例，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from flask_mail import Mail, Message\r\n",
    "\r\n",
    "app = Flask(__name__)\r\n",
    "app.debug = True\r\n",
    "app.config['SECRET_KEY'] = 'a really really really really long secret key'\r\n",
    "app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:flask123@localhost/flask_app_db'\r\n",
    "app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True # ADD BY YULK  \r\n",
    "\r\n",
    "manager = Manager(app)\r\n",
    "manager.add_command('db', MigrateCommand)\r\n",
    "db = SQLAlchemy(app)\r\n",
    "migrate = Migrate(app, db)\r\n",
    "mail = Mail(app)  # NEW \r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "接下来，我们必须设置一些配置选项，让 `Flask-Mail` 知道要连接到哪个 `SMTP` 服务器。在 main2.py 文件中添加以下配置(突出显示更改) :\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "app.config['SECRET_KEY'] = 'a really really really really long secret key'\r\n",
    "app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:flask123@localhost/flask_app_db'\r\n",
    "app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True # ADD BY YULK  \r\n",
    "\r\n",
    "app.config['MAIL_SERVER'] = 'smtp.googlemail.com' #新内容 BEGIN\r\n",
    "app.config['MAIL_PORT'] = 587\r\n",
    "app.config['MAIL_USE_TLS'] = True\r\n",
    "app.config['MAIL_USERNAME'] = 'infooveriq@gmail.com'  # enter your email here\r\n",
    "app.config['MAIL_DEFAULT_SENDER'] = 'infooveriq@gmail.com' # enter your email here\r\n",
    "app.config['MAIL_PASSWORD'] = 'password' # enter your password here  #新内容 END\r\n",
    "\r\n",
    "manager = Manager(app)\r\n",
    "manager.add_command('db', MigrateCommand)\r\n",
    "db = SQLAlchemy(app)\r\n",
    "mail = Mail(app)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "这里我们正在使用 Gmail SMTP 服务器。请注意 Gmail 只允许你每天发送100-150封邮件。如果这还不够，你可以试试 SendGrid 或 MailChimp 这样的替代品。\r\n",
    "\r\n",
    "与其像上文这样在代码中硬编码电子邮件和密码。更好的方法是将电子邮件和密码存储在环境变量中。这样，如果电子邮件或密码更改，我们不必更新我们的代码。我们将在以后的课程中看到如何做到这一点。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 电子邮件基础\r\n",
    "\r\n",
    "为了撰写一封电子邮件，我们创建了一个 Message 类的实例，如下所示:\r\n",
    "\r\n",
    "`msg = Message(\"Subject\", sender=\"sender@example.com\", recipients=['recipient_1@example.com'])`\r\n",
    "\r\n",
    "如果您已经设置了 `MAIL_DEFAULT_SENDER` 配置项，那么在创建 Message 实例时就不需要显式地传递 sender。\r\n",
    "\r\n",
    "`msg = Message(\"Subject\", recipients=['recipient@example.com'])`\r\n",
    "\r\n",
    "使用 Message 实例的 body 属性设置 mail body:\r\n",
    "\r\n",
    "`msg.body = \"Mail body\"`\r\n",
    "\r\n",
    "如果邮件主体是 HTML，则将其设置为 HTML 属性。\r\n",
    "\r\n",
    "`msg.body = \"<p>Mail body</p>\"`\r\n",
    "\r\n",
    "最后，我们可以通过将 Message 实例传递给 Mail 实例的 send ()方法来发送邮件:\r\n",
    "\r\n",
    "`mail.send(msg)`\r\n",
    "\r\n",
    "现在让我们通过命令行发送电子邮件来测试我们的配置。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 发送测试邮件\r\n",
    "\r\n",
    "打开终端并输入以下命令:\r\n",
    "\r\n",
    "```bash\r\n",
    "(env) overiq@vm:~/flask_app$ python main2.py shell\r\n",
    ">>>\r\n",
    ">>> from main2 import mail, Message\r\n",
    ">>>\r\n",
    ">>> msg = Message(\"Subject\", recipients=[\"infooveriq@gmail.com\"])\r\n",
    ">>> msg.html = \"<h2>Email Heading</h2>\\n<p>Email Body</p>\"\r\n",
    ">>>\r\n",
    ">>> mail.send(msg)\r\n",
    ">>>\r\n",
    "```\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "关于成功，你会收到这样一封邮件:\r\n",
    "\r\n",
    "请注意，除非你禁用双重认证，并允许安全性较低的应用程序访问您的帐户，否则通过 Gmail SMTP 服务器发送电子邮件将无法工作，"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 集成电子邮件和我们的应用程序\r\n",
    "\r\n",
    "目前的情况是，每当用户提交反馈信息并将其保存到数据库中时，用户就会收到一条成功信息，这就是全部内容，很无聊，你不觉得吗？理想情况下，应用程序应该将反馈通知管理员或版主。我们开始吧。打开 main2.py 并修改 contact ()视图函数以发送电子邮件，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "@app.route('/contact/', methods=['get', 'post'])\r\n",
    "def contact():\r\n",
    "    #...        \r\n",
    "        db.session.commit()\r\n",
    "\r\n",
    "        msg = Message(\"Feedback\", recipients=[app.config['MAIL_USERNAME']])\r\n",
    "        msg.body = \"You have received a new feedback from {} <{}>.\".format(name, email)\r\n",
    "        mail.send(msg)\r\n",
    "\r\n",
    "        print(\"\\nData received. Now redirecting ...\")\r\n",
    "    #...\r\n",
    "```\r\n",
    "\r\n",
    "启动服务器并访问 http://localhost:5000/contact/。填写表格并点击提交。一旦成功，你会收到这样的邮件:\r\n",
    "\r\n",
    "您可能已经注意到，从按下提交按钮到后续请求的成功消息之间有很长的延迟。问题在于 `mail.send() `方法会在几秒钟内阻塞 view 函数的执行。因此，在 mail.send ()方法返回之前，不会执行用于重定向页面的代码。我们可以用线程来解决这个问题。\r\n",
    "\r\n",
    "同时，我们还将重构发送电子邮件的代码。现在，如果我们想在我们的代码中发送电子邮件到其他地方，我们必须复制和粘贴完全相同的代码行。通过在函数中包装发送电子邮件的逻辑，我们可以在代码中节省一些行。\r\n",
    "\r\n",
    "打开 main2.py，在索引路由之前添加以下代码，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from threading import Thread \r\n",
    "#...\r\n",
    "def shell_context():\r\n",
    "    import os, sys\r\n",
    "    return dict(app=app, os=os, sys=sys)\r\n",
    "\r\n",
    "manager.add_command(\"shell\", Shell(make_context=shell_context))\r\n",
    "\r\n",
    "def async_send_mail(app, msg):\r\n",
    "    with app.app_context():  # 创建应用程序上下文，最后 mail.send ()发送电子邮件\r\n",
    "        mail.send(msg)\r\n",
    "\r\n",
    "\r\n",
    "def send_mail(subject, recipient, template, **kwargs):\r\n",
    "    msg = Message(subject, sender=app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])\r\n",
    "    msg.html = render_template(template, **kwargs)   # 17 渲染了一个模板，并将其结果赋值给 `msg.html` 属性\r\n",
    "    thr = Thread(target=async_send_mail, args=[app, msg])   # 18 创建一个 `Thread` 对象\r\n",
    "    thr.start() #启动线程\r\n",
    "    return thr\r\n",
    "\r\n",
    "@app.route('/')\r\n",
    "def index():\r\n",
    "    return render_template('index.html', name='Jerry')\r\n",
    "#...\r\n",
    "\r\n",
    "```\r\n",
    "\r\n",
    "我们在这里做了很多改进。`send_mail()` 函数的作用是封装发送邮件的逻辑。它接受主题、收件人和电子邮件模板。您还可以将任何其他参数作为关键字参数传递给它。为什么要增加额外的参数？其他参数表示要传递给模板的数据。在第17行中，我们渲染了一个模板，并将其结果赋值给 `msg.html` 属性。在第18行，我们正在创建一个 `Thread` 对象，传递函数的名称(target=async_send_mail)，并且必须使用。下一行启动线程。当线程启动时，调用 `async_send_mail()` 。接下来是有趣的部分。在我们的代码中，我们第一次在应用程序之外(即视图函数之外)的新线程中工作。`with app.app_context()`: 创建应用程序上下文，最后 mail.send ()发送电子邮件。\r\n",
    "\r\n",
    "接下来，我们需要创建一个反馈电子邮件的模板。在模板目录中创建一个名为 mail 的目录。这个目录将存储我们的电子邮件模板。在目录中创建一个名为 feedback 的模板，其代码如下:\r\n",
    "\r\n",
    "templates/mail/feedback.html\r\n",
    "\r\n",
    "`<p>You have received a new feedback from {{ name }} &lt;{{ email }}&gt; </p>`\r\n",
    "\r\n",
    "修改 contact() view 函数使用 send_mail() 函数如下(突出显示更改) :\r\n",
    "\r\n",
    "```python\r\n",
    "@app.route('/contact/', methods=['get', 'post'])\r\n",
    "def contact():\r\n",
    "        #...\r\n",
    "        db.session.commit()\r\n",
    "        \r\n",
    "        send_mail(\"New Feedback\", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',\r\n",
    "                  name=name, email=email)  # 新增行\r\n",
    "\r\n",
    "        print(\"\\nData received. Now redirecting ...\")\r\n",
    "        #...\r\n",
    "```        \r\n",
    "在这些改变之后再次访问 http://localhost:5000/contact/ ，填写表格并点击提交，这次你不会有任何延迟。"
   ],
   "metadata": {}
  }
 ],
 "metadata": {
  "orig_nbformat": 4,
  "language_info": {
   "name": "python",
   "version": "3.9.6",
   "mimetype": "text/x-python",
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "pygments_lexer": "ipython3",
   "nbconvert_exporter": "python",
   "file_extension": ".py"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3.9.6 64-bit"
  },
  "interpreter": {
   "hash": "c644d696b95f5e0f4df3c6556741cf30bcf9ea6ca93c3e1f29fcf31d885534fc"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}